package com.pearl.security.multiloginreject.demo.session.redis;

import cn.hutool.core.util.StrUtil;
import com.pearl.security.multiloginreject.demo.util.LoginUtils;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.FindByIndexNameSessionRepository;

import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.util.Assert;

public class MyRedisIndexedSessionRepository implements MyFindByIndexNameSessionRepository<MyRedisIndexedSessionRepository.RedisSession>, MessageListener, InitializingBean, DisposableBean {
    private static final Log logger = LogFactory.getLog(MyRedisIndexedSessionRepository.class);
    private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
    public static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
    public static final int DEFAULT_DATABASE = 0;
    public static final String DEFAULT_NAMESPACE = "spring:session";
    private int database = 0;
    private String namespace = "spring:session:";
    private String sessionCreatedChannelPrefix;
    private byte[] sessionCreatedChannelPrefixBytes;
    private String sessionDeletedChannel;
    private byte[] sessionDeletedChannelBytes;
    private String sessionExpiredChannel;
    private byte[] sessionExpiredChannelBytes;
    private String expiredKeyPrefix;
    private byte[] expiredKeyPrefixBytes;
    private final RedisOperations<String, Object> sessionRedisOperations;
    private final MyRedisSessionExpirationPolicy expirationPolicy;
    private ApplicationEventPublisher eventPublisher = (event) -> {
    };
    private Duration defaultMaxInactiveInterval = Duration.ofSeconds(1800L);
    private IndexResolver<Session> indexResolver = new DelegatingIndexResolver(new IndexResolver[]{new MyPrincipalNameIndexResolver()});
    private RedisSerializer<Object> defaultSerializer = new JdkSerializationRedisSerializer();
    private FlushMode flushMode;
    private SaveMode saveMode;
    private String cleanupCron;
    private ThreadPoolTaskScheduler taskScheduler;

    public MyRedisIndexedSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
        this.flushMode = FlushMode.ON_SAVE;
        this.saveMode = SaveMode.ON_SET_ATTRIBUTE;
        this.cleanupCron = "0 * * * * *";
        Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
        this.sessionRedisOperations = sessionRedisOperations;
        this.expirationPolicy = new MyRedisSessionExpirationPolicy(sessionRedisOperations, this::getExpirationsKey, this::getSessionKey);
        this.configureSessionChannels();
    }

    public void afterPropertiesSet() {
        if (!"-".equals(this.cleanupCron)) {
            this.taskScheduler = createTaskScheduler();
            this.taskScheduler.initialize();
            this.taskScheduler.schedule(this::cleanUpExpiredSessions, new CronTrigger(this.cleanupCron));
        }

    }

    private static ThreadPoolTaskScheduler createTaskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setThreadNamePrefix("spring-session-");
        return taskScheduler;
    }

    public void destroy() {
        if (this.taskScheduler != null) {
            this.taskScheduler.destroy();
        }

    }

    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
        this.eventPublisher = applicationEventPublisher;
    }

    public void setDefaultMaxInactiveInterval(Duration defaultMaxInactiveInterval) {
        Assert.notNull(defaultMaxInactiveInterval, "defaultMaxInactiveInterval must not be null");
        this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
    }

    /**
     * @deprecated
     */
    @Deprecated(
            since = "3.0.0"
    )
    public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
        this.setDefaultMaxInactiveInterval(Duration.ofSeconds((long) defaultMaxInactiveInterval));
    }

    public void setIndexResolver(IndexResolver<Session> indexResolver) {
        Assert.notNull(indexResolver, "indexResolver cannot be null");
        this.indexResolver = indexResolver;
    }

    public void setDefaultSerializer(RedisSerializer<Object> defaultSerializer) {
        Assert.notNull(defaultSerializer, "defaultSerializer cannot be null");
        this.defaultSerializer = defaultSerializer;
    }

    public void setFlushMode(FlushMode flushMode) {
        Assert.notNull(flushMode, "flushMode cannot be null");
        this.flushMode = flushMode;
    }

    public void setSaveMode(SaveMode saveMode) {
        Assert.notNull(saveMode, "saveMode must not be null");
        this.saveMode = saveMode;
    }

    public void setCleanupCron(String cleanupCron) {
        Assert.notNull(cleanupCron, "cleanupCron must not be null");
        if (!"-".equals(cleanupCron)) {
            Assert.isTrue(CronExpression.isValidExpression(cleanupCron), "cleanupCron must be valid");
        }

        this.cleanupCron = cleanupCron;
    }

    public void setDatabase(int database) {
        this.database = database;
        this.configureSessionChannels();
    }

    private void configureSessionChannels() {
        this.sessionCreatedChannelPrefix = this.namespace + "event:" + this.database + ":created:";
        this.sessionCreatedChannelPrefixBytes = this.sessionCreatedChannelPrefix.getBytes();
        this.sessionDeletedChannel = "__keyevent@" + this.database + "__:del";
        this.sessionDeletedChannelBytes = this.sessionDeletedChannel.getBytes();
        this.sessionExpiredChannel = "__keyevent@" + this.database + "__:expired";
        this.sessionExpiredChannelBytes = this.sessionExpiredChannel.getBytes();
        this.expiredKeyPrefix = this.namespace + "sessions:expires:";
        this.expiredKeyPrefixBytes = this.expiredKeyPrefix.getBytes();
    }

    public RedisOperations<String, Object> getSessionRedisOperations() {
        return this.sessionRedisOperations;
    }

    public void save(MyRedisIndexedSessionRepository.RedisSession session) {
        session.save();
    }

    public void cleanUpExpiredSessions() {
        this.expirationPolicy.cleanExpiredSessions();
    }

    public MyRedisIndexedSessionRepository.RedisSession findById(String id) {
        return this.getSession(id, false);
    }

    public Map<String, MyRedisIndexedSessionRepository.RedisSession> findByIndexNameAndIndexValue(String indexName, Object indexValue) {
        if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
            return Collections.emptyMap();
        } else {
            String principalKey = this.getPrincipalKey(indexValue);
            Set<Object> sessionIds = this.sessionRedisOperations.boundSetOps(principalKey).members();
            if (sessionIds == null) {
                return Collections.emptyMap();
            } else {
                Map<String, MyRedisIndexedSessionRepository.RedisSession> sessions = new HashMap<>(sessionIds.size());
                for (Object id : sessionIds) {
                    RedisSession session = this.findById((String) id);
                    if (session != null) {
                        sessions.put(session.getId(), session);
                    }
                }

                return sessions;
            }
        }
    }

    private MyRedisIndexedSessionRepository.RedisSession getSession(String id, boolean allowExpired) {
        Map<String, Object> entries = this.getSessionBoundHashOperations(id).entries();
        if (entries != null && !entries.isEmpty()) {
            MapSession loaded = (new RedisSessionMapper(id)).apply(entries);
            if (!allowExpired && loaded.isExpired()) {
                return null;
            } else {
                MyRedisIndexedSessionRepository.RedisSession result = new MyRedisIndexedSessionRepository.RedisSession(loaded, false);
                result.originalLastAccessTime = loaded.getLastAccessedTime();
                return result;
            }
        } else {
            return null;
        }
    }

    public void deleteById(String sessionId) {
        MyRedisIndexedSessionRepository.RedisSession session = this.getSession(sessionId, true);
        if (session != null) {
            this.cleanupPrincipalIndex(session);
            this.expirationPolicy.onDelete(session);
            String expireKey = this.getExpiredKey(session.getId());
            this.sessionRedisOperations.delete(expireKey);
            session.setMaxInactiveInterval(Duration.ZERO);
            this.save(session);
        }
    }

    public MyRedisIndexedSessionRepository.RedisSession createSession() {
        MapSession cached = new MapSession();
        cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
        MyRedisIndexedSessionRepository.RedisSession session = new MyRedisIndexedSessionRepository.RedisSession(cached, true);
        session.flushImmediateIfNecessary();
        return session;
    }

    public void onMessage(Message message, byte[] pattern) {
        byte[] messageChannel = message.getChannel();
        if (ByteUtils.startsWith(messageChannel, this.sessionCreatedChannelPrefixBytes)) {
            String channel = new String(messageChannel);
            String sessionId = channel.substring(channel.lastIndexOf(":") + 1);
            Map<String, Object> entries = (Map) this.defaultSerializer.deserialize(message.getBody());
            MapSession loaded = (new RedisSessionMapper(sessionId)).apply(entries);
            MyRedisIndexedSessionRepository.RedisSession session = new MyRedisIndexedSessionRepository.RedisSession(loaded, false);
            this.handleCreated(session);
        } else {
            byte[] messageBody = message.getBody();
            if (ByteUtils.startsWith(messageBody, this.expiredKeyPrefixBytes)) {
                boolean isDeleted = Arrays.equals(messageChannel, this.sessionDeletedChannelBytes);
                if (isDeleted || Arrays.equals(messageChannel, this.sessionExpiredChannelBytes)) {
                    String body = new String(messageBody);
                    int beginIndex = body.lastIndexOf(":") + 1;
                    int endIndex = body.length();
                    String sessionId = body.substring(beginIndex, endIndex);
                    MyRedisIndexedSessionRepository.RedisSession session = this.getSession(sessionId, true);
                    if (session == null) {
                        logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId);
                        return;
                    }

                    if (logger.isDebugEnabled()) {
                        logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
                    }

                    this.cleanupPrincipalIndex(session);
                    if (isDeleted) {
                        this.handleDeleted(session);
                    } else {
                        this.handleExpired(session);
                    }
                }

            }
        }
    }

    private void cleanupPrincipalIndex(MyRedisIndexedSessionRepository.RedisSession session) {
        String sessionId = session.getId();
        Map<String, String> indexes = this.indexResolver.resolveIndexesFor(session);
        String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
        if (principal != null) {
            this.sessionRedisOperations.boundSetOps(this.getPrincipalKey(principal)).remove(new Object[]{sessionId});
        }

    }

    private void handleCreated(MyRedisIndexedSessionRepository.RedisSession session) {
        this.publishEvent(new SessionCreatedEvent(this, session));
    }

    private void handleDeleted(MyRedisIndexedSessionRepository.RedisSession session) {
        this.publishEvent(new SessionDeletedEvent(this, session));
    }

    private void handleExpired(MyRedisIndexedSessionRepository.RedisSession session) {
        this.publishEvent(new SessionExpiredEvent(this, session));
    }

    private void publishEvent(ApplicationEvent event) {
        try {
            this.eventPublisher.publishEvent(event);
        } catch (Throwable var3) {
            logger.error("Error publishing " + event + ".", var3);
        }

    }

    public void setRedisKeyNamespace(String namespace) {
        Assert.hasText(namespace, "namespace cannot be null or empty");
        this.namespace = namespace.trim() + ":";
        this.configureSessionChannels();
    }

    String getSessionKey(String sessionId) {
        return this.namespace + "sessions:" + sessionId;
    }



    String getPrincipalKey(Object principal) {
        return this.namespace + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":" + LoginUtils.getKey(principal);
    }

    String getPrincipalKey(String principalName) {
        return this.namespace + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":" + principalName;
    }

    String getExpirationsKey(long expiration) {
        return this.namespace + "expirations:" + expiration;
    }

    private String getExpiredKey(String sessionId) {
        String var10000 = this.getExpiredKeyPrefix();
        return var10000 + sessionId;
    }

    private String getSessionCreatedChannel(String sessionId) {
        String var10000 = this.getSessionCreatedChannelPrefix();
        return var10000 + sessionId;
    }

    private String getExpiredKeyPrefix() {
        return this.expiredKeyPrefix;
    }

    public String getSessionCreatedChannelPrefix() {
        return this.sessionCreatedChannelPrefix;
    }

    public String getSessionDeletedChannel() {
        return this.sessionDeletedChannel;
    }

    public String getSessionExpiredChannel() {
        return this.sessionExpiredChannel;
    }

    private BoundHashOperations<String, String, Object> getSessionBoundHashOperations(String sessionId) {
        String key = this.getSessionKey(sessionId);
        return this.sessionRedisOperations.boundHashOps(key);
    }

    static String getSessionAttrNameKey(String attributeName) {
        return "sessionAttr:" + attributeName;
    }

    final class RedisSession implements Session {
        private final MapSession cached;
        private Instant originalLastAccessTime;
        private Map<String, Object> delta = new HashMap();
        private boolean isNew;
        private String originalPrincipalName; // 修改为唯一标识 PC:123456
        private String originalSessionId;

        RedisSession(MapSession cached, boolean isNew) {
            this.cached = cached;
            this.isNew = isNew;
            this.originalSessionId = cached.getId();
            Map<String, String> indexes = MyRedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
            this.originalPrincipalName = (String) indexes.get(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
            if (this.isNew) {
                this.delta.put("creationTime", cached.getCreationTime().toEpochMilli());
                this.delta.put("maxInactiveInterval", (int) cached.getMaxInactiveInterval().getSeconds());
                this.delta.put("lastAccessedTime", cached.getLastAccessedTime().toEpochMilli());
            }

            if (this.isNew || MyRedisIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS) {
                this.getAttributeNames().forEach((attributeName) -> {
                    this.delta.put(MyRedisIndexedSessionRepository.getSessionAttrNameKey(attributeName), cached.getAttribute(attributeName));
                });
            }

        }

        public void setLastAccessedTime(Instant lastAccessedTime) {
            this.cached.setLastAccessedTime(lastAccessedTime);
            this.delta.put("lastAccessedTime", this.getLastAccessedTime().toEpochMilli());
            this.flushImmediateIfNecessary();
        }

        public boolean isExpired() {
            return this.cached.isExpired();
        }

        public Instant getCreationTime() {
            return this.cached.getCreationTime();
        }

        public String getId() {
            return this.cached.getId();
        }

        public String changeSessionId() {
            return this.cached.changeSessionId();
        }

        public Instant getLastAccessedTime() {
            return this.cached.getLastAccessedTime();
        }

        public void setMaxInactiveInterval(Duration interval) {
            this.cached.setMaxInactiveInterval(interval);
            this.delta.put("maxInactiveInterval", (int) this.getMaxInactiveInterval().getSeconds());
            this.flushImmediateIfNecessary();
        }

        public Duration getMaxInactiveInterval() {
            return this.cached.getMaxInactiveInterval();
        }

        public <T> T getAttribute(String attributeName) {
            T attributeValue = this.cached.getAttribute(attributeName);
            if (attributeValue != null && MyRedisIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
                this.delta.put(MyRedisIndexedSessionRepository.getSessionAttrNameKey(attributeName), attributeValue);
            }

            return attributeValue;
        }

        public Set<String> getAttributeNames() {
            return this.cached.getAttributeNames();
        }

        public void setAttribute(String attributeName, Object attributeValue) {
            this.cached.setAttribute(attributeName, attributeValue);
            this.delta.put(MyRedisIndexedSessionRepository.getSessionAttrNameKey(attributeName), attributeValue);
            this.flushImmediateIfNecessary();
        }

        public void removeAttribute(String attributeName) {
            this.cached.removeAttribute(attributeName);
            this.delta.put(MyRedisIndexedSessionRepository.getSessionAttrNameKey(attributeName), (Object) null);
            this.flushImmediateIfNecessary();
        }

        private void flushImmediateIfNecessary() {
            if (MyRedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
                this.save();
            }

        }

        private void save() {
            this.saveChangeSessionId();
            this.saveDelta();
        }

        private void saveDelta() {
            if (!this.delta.isEmpty()) {
                String sessionId = this.getId();
                MyRedisIndexedSessionRepository.this.getSessionBoundHashOperations(sessionId).putAll(this.delta);
                String principalSessionKey = MyRedisIndexedSessionRepository.getSessionAttrNameKey(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
                String securityPrincipalSessionKey = MyRedisIndexedSessionRepository.getSessionAttrNameKey("SPRING_SECURITY_CONTEXT");
                String sessionCreatedKey;
                if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
                    if (this.originalPrincipalName != null) {
                        sessionCreatedKey = MyRedisIndexedSessionRepository.this.getPrincipalKey(this.originalPrincipalName);
                        MyRedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(sessionCreatedKey).remove(new Object[]{sessionId});
                    }
                    // 会话中解析出账号
                    Map<String, String> indexes = MyRedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
                    String principal = indexes.get(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
                    // 保存index
                    this.originalPrincipalName = principal; // 账号
                    if (principal != null) {
                        String principalRedisKey = MyRedisIndexedSessionRepository.this.getPrincipalKey(principal);
                        MyRedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey).add(new Object[]{sessionId});
                    }
                }

                if (this.isNew) {
                    sessionCreatedKey = MyRedisIndexedSessionRepository.this.getSessionCreatedChannel(this.getId());
                    MyRedisIndexedSessionRepository.this.sessionRedisOperations.convertAndSend(sessionCreatedKey, this.delta);
                    this.isNew = false;
                }

                this.delta = new HashMap(this.delta.size());
                Long originalExpiration = this.originalLastAccessTime != null ? this.originalLastAccessTime.plus(this.getMaxInactiveInterval()).toEpochMilli() : null;
                MyRedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
            }
        }

        private void saveChangeSessionId() {
            String sessionId = this.getId();
            if (!sessionId.equals(this.originalSessionId)) {
                if (!this.isNew) {
                    String originalSessionIdKey = MyRedisIndexedSessionRepository.this.getSessionKey(this.originalSessionId);
                    String sessionIdKey = MyRedisIndexedSessionRepository.this.getSessionKey(sessionId);

                    try {
                        MyRedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey, sessionIdKey);
                    } catch (NonTransientDataAccessException var8) {
                        this.handleErrNoSuchKeyError(var8);
                    }

                    String originalExpiredKey = MyRedisIndexedSessionRepository.this.getExpiredKey(this.originalSessionId);
                    String expiredKey = MyRedisIndexedSessionRepository.this.getExpiredKey(sessionId);

                    try {
                        MyRedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
                    } catch (NonTransientDataAccessException var7) {
                        this.handleErrNoSuchKeyError(var7);
                    }

                    if (this.originalPrincipalName != null) {
                        String originalPrincipalRedisKey = MyRedisIndexedSessionRepository.this.getPrincipalKey(this.originalPrincipalName);
                        MyRedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey).remove(new Object[]{this.originalSessionId});
                        MyRedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey).add(new Object[]{sessionId});
                    }
                }

                this.originalSessionId = sessionId;
            }
        }

        private void handleErrNoSuchKeyError(NonTransientDataAccessException ex) {
            if (!"ERR no such key".equals(NestedExceptionUtils.getMostSpecificCause(ex).getMessage())) {
                throw ex;
            }
        }
    }
}
